首先先來定義一下什麼是資料庫正規化:
資料庫正規化的過程,其實就是將一些實體的描述資料,透過一定的程序,將表單簡化,直到一張表單只單純描述一個事實為止。
簡單來說,也就是透過一定的程序,去除資料庫中冗餘的內容,讓資料能夠井然有序且有效率的儲存。
經過正規化後的資料庫,應具備以下特性:
我們先來看一下這張完全不符合正規化特性的table,相信後端的朋友可能在剛開始學資料庫的時候可能也設計過類似這樣的table。
Comsumptions
姓名 | 性別 | gender | 項目 | 價格 | 數量 | 商店 | 地址 | 日期 | 訂單編號 |
---|---|---|---|---|---|---|---|---|---|
阿寶 | 女 | F | 鉛筆、橡皮擦 | 20, 50 | 1,2 | 久成久 | 東區大學路 | 12/17 | 1 |
豆芽 | 女 | F | 牛奶、三明治 | 70, 10 | 2,1 | 全家 | 東區北門路 | 12/18 | 2 |
豆芽 | 女 | F | 牛奶、三明治 | 70, 10 | 3,4 | 全家 | 東區北門路 | 12/19 | 3 |
老皮 | 男 | M | 蛋餅、奶茶 | 30, 40 | 1,2 | 日蝕 | 東區成功路 | 12/19 | 4 |
這張名為consumptions的table裡頭儲存的是使用者的消費紀錄。
很明顯的,有幾個大問題存在於這張表中:
首先:他在項目和價格這兩個columns裡面都出現了一個欄位儲存兩項以上的資料的問題,分隔這些資料的符號甚至都不統一。
第二:性別和gender這兩個column,他們在意義上是重複的。我們其實可以保留其中一個好了,不必浪費空間跟時間來儲存多餘的資料。
第三:這些資料並沒有一個primary key可以辨識他們的區別,這些欄位都有可能是重複的。例如第二筆和第三筆資料的內容長的一模一樣,假設我今天想要查詢豆芽第一次在全家消費牛奶的紀錄,我就沒辦法依據這些資料的內容來找到這比消費紀錄。
問題:
- 一個欄位儲存多筆資料
- 出現意義上重複的欄位
- 缺乏主鍵(Primary Key)
因此我們要透過第一正規化來修正以上的問題。
第一NF要完成的工作:
- 一個欄位只能有單一值
- 消除意義上重複的欄位
- 決定主鍵
第一正規化會把原本一個欄位儲存多項資料的部份分開來儲存,然後刪除意義重複的column,最後給每一筆資料一個primary key來當作他們的標識。
經過第一正規化後的表就會長成這樣,我們把每一項商品的消費都分開來儲存,然後把多餘的欄位刪掉。
現在這張表中的每個欄位都會是有意義的。
定義pk為姓名、項目、數量、商店、日期和訂單編號(複合主鍵)
comsumptions
| 姓名 | 性別| 項目 |價格|數量|總金額| 商店 | 地址 | 日期 |訂單編號
| --|---|--- |---|-- | ----|--|---- |---|
| 阿寶 | 女 | 鉛筆|20|1|20|久成久|東區大學路|12/17|1
| 阿寶 | 女 | 橡皮擦|50|2|100|久成久|東區大學路|12/17|1
| 豆芽 | 女 | 牛奶|70|3|210|全家|東區北門路|12/18|2
| 豆芽 | 女 | 三明治|10|1|10|全家|東區北門路|12/18|2
| 豆芽 | 女 | 牛奶|70|3|210|全家|東區北門路|12/19|3
| 豆芽 | 女 | 三明治|10|4|40|全家|東區北門路|12/19|3
| 老皮 | 男 | 蛋餅|30|1|30|日蝕|東區成功路|12/19|4
| 老皮 | 男 | 奶茶|40|2|80|日蝕|東區成功路|12/19|4
然而,這張表還是存在著一些問題:他的每一筆消費紀錄都要紀錄消費者的性別、商店的名稱和地址,重複內容過多。
假設今天老皮在日蝕買了一百個蛋餅,那我就要重複輸入:老皮,性別為男性,商店的地址為東區成功路,非常沒有效率。
又如果這間日蝕搬到了民族路上,豈不是還要在這一百筆資料裡一筆一筆做修改?
問題:
- 出現過多重複資料
因此我們要透過第二正規化來修正上述的問題
第二NF要完成的工作:
- 消除部分相依
部分相依的意思為跟主鍵只有一部份有關係,另一部份沒有關係的欄位,我們要把這些欄位獨立於另一張表。
很多框架在建表時預設會以id作為pk,若該表以完成第一正規化,設定id為pk則直接滿足第二正規化,因為pk為單一鍵(已經是最小單位了,自然不會有和pk"部分相依的情況")
但這邊的主鍵指的不是形式上的'id'這個欄位,而是能用來辨識資料區別的複合主鍵,例如購物紀錄中的消費者、物品、價錢、時間這些欄位所組成的複合主鍵。(我自己是將id理解成那些複合主鍵的代碼,畢竟在大部分的情況下我們所稱的主鍵就是primary key,就是像id這種欄位)
舉這邊的例子來看,第二正規化要新增一個獨立的table來儲存這些一直重複出現的欄位,接著透過foreign key來關聯這些table
第二正規化後,另外建了兩個獨立的表,items跟consumers來儲存消費者資訊和商品資訊。
consumptions分別和items跟consumers關聯,把這些資料分出去儲存後,orders就單純的只儲存消費者和購買的商品,以及消費日期三種資料而已。
在orders中,消費者對應到consumers的姓名,商品對應到items的名稱,而消費者和商品就是orders的foreign key。
consumers
|id| 姓名 | 性別|
|1|---|---|
|2|阿寶 | 女
|3|豆芽 | 女
|4|老皮 | 男 |
shops
id | 商店 | 地址 |
---|---|---|
1 | 久成久 | 東區大學路 |
2 | 全家 | 東區北門路 |
3 | 日蝕 | 東區成功路 |
items
id | 項目 | 價格 | 商店id |
---|---|---|---|
1 | 鉛筆 | 20 | 1 |
2 | 橡皮擦 | 50 | 1 |
3 | 牛奶 | 70 | 2 |
4 | 三明治 | 10 | 2 |
5 | 蛋餅 | 30 | 3 |
6 | 奶茶 | 40 | 3 |
orders
id | 消費者id | 商品id | 數量 | 總金額 | 日期 | 訂單編號 |
---|---|---|---|---|---|---|
1 | 1 | 1 | 1 | 20 | 12/17 | 1 |
2 | 1 | 2 | 2 | 100 | 12/17 | 1 |
3 | 2 | 3 | 3 | 210 | 12/18 | 2 |
4 | 2 | 4 | 1 | 10 | 12/18 | 2 |
5 | 2 | 3 | 3 | 210 | 12/19 | 3 |
6 | 3 | 4 | 4 | 40 | 12/19 | 3 |
7 | 3 | 5 | 1 | 30 | 12/19 | 4 |
8 | 3 | 6 | 2 | 80 | 12/19 | 4 |
然而,這樣還是會有一個問題,就是遞移關係。
所謂遞移關係,在這個範例裡就是指:總金額是依賴商品及數量的資訊,而商品id和數量又和主鍵直接相關,那總金額和主鍵之間的關係就是遞移關係。
用個比較容易理解的方式來說明:為了避免數量改變而總金額沒改到造成資料錯誤,應該把總金額那個欄位移除。
問題:存在主鍵以外的欄位與主鍵間接(遞移)相依
在第三正規化的規範中,要消除資料表中與主欄位的遞移相依
第三NF要完成的工作:
- 消除資料表中的遞移相依
遞移相依:欄位1和主鍵相關,欄位2和欄位1相關,欄位2和主鍵就是地移相依
候選鍵:欄位組合讓資料能是唯一的,並且是最小唯一
主鍵為訂單編號,而總金額和非主鍵欄位商品、數量存在遞移相依
consumers
id | 姓名 | 性別 |
---|---|---|
1 | 阿寶 | 女 |
2 | 豆芽 | 女 |
3 | 老皮 | 男 |
shops
id | 商店 | 地址 |
---|---|---|
1 | 久成久 | 東區大學路 |
2 | 全家 | 東區北門路 |
3 | 日蝕 | 東區成功路 |
items
id | 項目 | 價格 | 商店id |
---|---|---|---|
1 | 鉛筆 | 20 | 1 |
2 | 橡皮擦 | 50 | 1 |
3 | 牛奶 | 70 | 2 |
4 | 三明治 | 10 | 2 |
5 | 蛋餅 | 30 | 3 |
6 | 奶茶 | 40 | 3 |
orders
|id|消費者id|單價|數量 |商品id | 日期 |訂單編號
|--|---| ---|---|----|--|
|1|1| 20|1|1|12/17|1
|2|1| 50|2|2|12/17|1
|3|2| 70|3|3|12/18|2
|4|2| 10|1|4|12/18|2
|5|2| 70|3|3|12/19|3
|6|3| 10|4|4|12/19|3
|7|3|30|1|5|12/19|4
|8|3| 40|2|6|12/19|4
但是,並不是每次建表都要照著正規化的規範才是最好的。
像前面這個範例,假如我的資料量不大,但我的業務需求是要反覆的去查詢某個消費者買的什麼商品,是在什麼地方的資料。
例如,我想找到今天豆芽買牛奶的地址,我必須先從consumptions關聯到items,再關聯到shops。
這樣似乎反而比第二正規化前更缺乏效率。
因此,資料庫正規化要進行到什麼程度,應視對資料庫操作的需求和資料量而定,而不是每次建表都只要無腦的全部照著正規化的規範去設計你的表格就好。